﻿/*
 * @author: wizardc
 */

using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using Dou.Download;
using Dou.Sync;
using Dou.Utils;
using Newtonsoft.Json;
using UnityEngine;

namespace Dou.Resource
{
    /// <summary>
    /// 热更新管理器
    /// 目前仅将最基本的热更流程跑通，没有处理其它情况，下面是待优化项：
    /// 1. 热更中断后，重新进游戏从中断处继续进行热更；
    /// 2. 需要考虑热更中断之后，再热更时已经是新热更包的特殊情况处理；
    /// </summary>
    public sealed class HotFixedManager
    {
        private static HotFixedManager _instance;
        public static HotFixedManager instance => _instance ??= new HotFixedManager();

        /// <summary>
        /// 热更新版本文件名
        /// </summary>
        private const string HotFixedVersionFile = "HotFixedVersion";
        
        /// <summary>
        /// 资源配置相关文件名
        /// </summary>
        private const string HotFixedManifest = "hfm.list";
        
        /// <summary>
        /// 请求超时时间
        /// </summary>
        private const int timeout = 15;
        
        /// <summary>
        /// 加载失败时的重试次数
        /// </summary>
        private const int retryCount = 5;
        
        /// <summary>
        /// 下载线程数量
        /// </summary>
        private const int downloadThread = 6;

        /// <summary>
        /// 需要重新下载安装包时
        /// </summary>
        public event Action onNewVersion;
        
        /// <summary>
        /// 热更步骤变更时
        /// </summary>
        public event Action<HotFixedStep> onStepChanged;
        
        /// <summary>
        /// 热更信息正常变更时
        /// </summary>
        public event Action<HotFixedChangeInfo> onChanged;
        
        /// <summary>
        /// 热更正常新完成时
        /// </summary>
        public event Action onComplete;
        
        /// <summary>
        /// 热更新出现异常时
        /// </summary>
        public event Action<HotFixedChangeInfo> onError;

        private HotFixedStep _step;
        
        private VersionFile _version;
        private string _hotFixedVersionPath;
        
        private HotFixedManifest _hotFixedManifest;

        private HotFixedData _hotFixedData;
        private List<HotFixedFile> _changedFiles;
        private FileDownloadInfo _fileDownloadInfo;
        private int _verifyCount;
        private bool _verifyComplete;
        
        private HotFixedManager()
        {
            _hotFixedVersionPath = Path.Combine(AssetUtil.HotFixedDir, HotFixedVersionFile);
        }

        /// <summary>
        /// 热更信息
        /// </summary>
        public HotFixedManifest hotFixedManifest => _hotFixedManifest;
        
        /// <summary>
        /// 开始热更流程
        /// </summary>
        /// <param name="uri">拉取热更关键信息的地址</param>
        public void CheckHotFixed(string uri)
        {
            _step = HotFixedStep.GetVersion;
            onStepChanged?.Invoke(_step);
            
            GetNowManifest();
            GetNowVersion();
            
            HttpUtil.Get(uri, (code, response) =>
            {
                _hotFixedData = JsonConvert.DeserializeObject<HotFixedData>(response);
                // 需要下载新包
                if (_version.mainVersion != _hotFixedData.mainVersion || _version.subVersion != _hotFixedData.subVersion)
                {
                    onNewVersion?.Invoke();
                    return;
                }
                // 无需热更新
                if (_version.hotFixedVersion == _hotFixedData.hotFixedVersion)
                {
                    onComplete?.Invoke();
                    return;
                }
                // 记录版本
                _version.hotFixedVersion = _hotFixedData.hotFixedVersion;
                // 开始热更
                DownloadHotFixedManifest();
            }, (code, error) =>
            {
                onChanged?.Invoke(new HotFixedChangeInfo
                {
                    step = _step,
                    error = HotFixedErrorCode.GetVersionFailed
                });
            }, timeout, retryCount);
        }

        private void GetNowManifest()
        {
            var hotFixedManifestPath = AssetUtil.GetFilePathInHotFix(HotFixedManifest);
            if (File.Exists(hotFixedManifestPath))
            {
                _hotFixedManifest = new HotFixedManifest();
                _hotFixedManifest.Decode(File.ReadAllText(hotFixedManifestPath));
            }
            else
            {
                _hotFixedManifest = new HotFixedManifest();
            }
        }

        private void GetNowVersion()
        {
            _version = Resources.Load<VersionFile>("Version");
            if (File.Exists(_hotFixedVersionPath))
            {
                if (int.TryParse(File.ReadAllText(_hotFixedVersionPath), out var version))
                {
                    _version.hotFixedVersion = version;
                }
            }
        }

        private void SaveHotFixedVersion()
        {
            File.WriteAllBytes(_hotFixedVersionPath, UTF8Encoding.UTF8.GetBytes(_version.hotFixedVersion.ToString()));
        }

        private void DownloadHotFixedManifest()
        {
            _step = HotFixedStep.GetManifest;
            onStepChanged?.Invoke(_step);
            
            HttpUtil.Get(_hotFixedData.hotFixedManifestUrl, (code, response) =>
            {
                var nowHotFixedManifest = _hotFixedManifest;
                var newHotFixedManifest = new HotFixedManifest();
                newHotFixedManifest.Decode(response);
                _hotFixedManifest = newHotFixedManifest;
                GetHotFixedFiles(nowHotFixedManifest, newHotFixedManifest);
            }, (code, error) =>
            {
                onChanged?.Invoke(new HotFixedChangeInfo
                {
                    step = _step,
                    error = HotFixedErrorCode.GetManifestFailed
                });
            }, timeout, retryCount);
        }

        private void GetHotFixedFiles(HotFixedManifest nowManifest, HotFixedManifest newManifest)
        {
            _changedFiles = new List<HotFixedFile>();
            foreach (var kv in newManifest.fileInfoMap)
            {
                var fileInfo = kv.Value;
                if (nowManifest.fileInfoMap.ContainsKey(fileInfo.fileName))
                {
                    var oldFileInfo = nowManifest.fileInfoMap[fileInfo.fileName];
                    if (fileInfo.hashCode != oldFileInfo.hashCode)
                    {
                        _changedFiles.Add(new HotFixedFile
                        {
                            changeType = HotFixedChangeType.Modify,
                            fileName = fileInfo.fileName,
                            hashCode = fileInfo.hashCode,
                            size = fileInfo.size,
                        });
                    }
                }
                else
                {
                    _changedFiles.Add(new HotFixedFile
                    {
                        changeType = HotFixedChangeType.Add,
                        fileName = fileInfo.fileName,
                        hashCode = fileInfo.hashCode,
                        size = fileInfo.size,
                    });
                }
            }
            foreach (var kv in nowManifest.fileInfoMap)
            {
                var fileInfo = kv.Value;
                if (!newManifest.fileInfoMap.ContainsKey(fileInfo.fileName))
                {
                    _changedFiles.Add(new HotFixedFile
                    {
                        changeType = HotFixedChangeType.Delete,
                        fileName = fileInfo.fileName,
                        url = _hotFixedData.hotFixedUrl + "/" + fileInfo.fileName,
                        diskPath = AssetUtil.HotFixedDir,
                        hashCode = fileInfo.hashCode,
                        size = fileInfo.size,
                    });
                }
            }
            DownloadHotFixed();
        }

        private void DownloadHotFixed()
        {
            _step = HotFixedStep.DownloadResource;
            onStepChanged?.Invoke(_step);
            
            _fileDownloadInfo = new FileDownloadInfo();
            
            var downloadManager = DownloadManager.instance;
            downloadManager.retryCount = retryCount;
            downloadManager.maxDownloadThread = downloadThread;
            foreach (var hotFixedFile in _changedFiles)
            {
                _fileDownloadInfo.AddDownload(hotFixedFile.url, hotFixedFile.fileName, hotFixedFile.size);
                downloadManager.Download(hotFixedFile.url, hotFixedFile.diskPath, hotFixedFile.size, OnDownloadComplete, OnDownLoadProgress, OnDownLoadError);
            }
        }

        private void OnDownloadComplete(string url, string filePath)
        {
            _fileDownloadInfo.OnDownloadComplete(url);
            if (_fileDownloadInfo.allComplete)
            {
                _fileDownloadInfo = null;
                VerifyDownloadFile();
            }
        }

        private void OnDownLoadProgress(string url, string filePath, long currentBytes, long totalBytes)
        {
            _fileDownloadInfo.OnDownLoadProgress(url, currentBytes);
            onChanged?.Invoke(new HotFixedChangeInfo
            {
                step = _step,
                error = HotFixedErrorCode.None,
                fileName = _fileDownloadInfo.fileName,
                currentSize = _fileDownloadInfo.currentSize,
                totalSize = _fileDownloadInfo.totalSize,
                downloadFile = _fileDownloadInfo.downloadFile,
                downloadingFile = _fileDownloadInfo.downloadingFile,
                totalFile = _fileDownloadInfo.totalFile
            });
        }

        private void OnDownLoadError(string url, string filePath, DownloadErrorCode errorCode, string errorMsg)
        {
            _fileDownloadInfo.OnDownLoadError(url, errorMsg);
            onChanged?.Invoke(new HotFixedChangeInfo
            {
                step = _step,
                error = HotFixedErrorCode.DownloadFailed,
                fileName = _fileDownloadInfo.fileName,
                currentSize = _fileDownloadInfo.currentSize,
                totalSize = _fileDownloadInfo.totalSize,
                downloadFile = _fileDownloadInfo.downloadFile,
                downloadingFile = _fileDownloadInfo.downloadingFile,
                totalFile = _fileDownloadInfo.totalFile
            });
        }

        private void VerifyDownloadFile()
        {
            _step = HotFixedStep.VerifyFile;
            onStepChanged?.Invoke(_step);
            
            _fileDownloadInfo = new FileDownloadInfo();
            foreach (var hotFixedFile in _changedFiles)
            {
                _fileDownloadInfo.AddDownload(hotFixedFile.diskPath, hotFixedFile.fileName, hotFixedFile.size);
            }

            for (int i = 0; i < downloadThread; i++)
            {
                Thread receiveThread = new Thread(VerifyMD5);
                receiveThread.IsBackground = true;
                receiveThread.Start();
            }
        }

        private void VerifyMD5()
        {
            while (true)
            {
                HotFixedFile file = null;
                lock (this)
                {
                    if (_verifyComplete)
                    {
                        break;
                    }
                    if (_verifyCount >= _changedFiles.Count)
                    {
                        _verifyComplete = true;
                        MainThreadContext.Post(OnVerifyComplete);
                        break;
                    }
                    file = _changedFiles[_verifyCount++];
                }
                var md5 = GetFileMD5(file.diskPath, checkSize =>
                {
                    MainThreadContext.Post(() =>
                    {
                        _fileDownloadInfo.OnDownLoadProgress(file.diskPath, checkSize);
                        onChanged?.Invoke(new HotFixedChangeInfo
                        {
                            step = _step,
                            error = HotFixedErrorCode.None,
                            fileName = _fileDownloadInfo.fileName,
                            currentSize = _fileDownloadInfo.currentSize,
                            totalSize = _fileDownloadInfo.totalSize,
                            downloadFile = _fileDownloadInfo.downloadFile,
                            downloadingFile = _fileDownloadInfo.downloadingFile,
                            totalFile = _fileDownloadInfo.totalFile
                        });
                    });
                });
                _fileDownloadInfo.OnDownloadComplete(file.diskPath);
                if (file.hashCode != md5)
                {
                    _verifyComplete = true;
                    MainThreadContext.Post(() =>
                    {
                        _fileDownloadInfo.OnDownLoadError(file.diskPath, file.fileName);
                        onChanged?.Invoke(new HotFixedChangeInfo
                        {
                            step = _step,
                            error = HotFixedErrorCode.VerifyFailed,
                            fileName = _fileDownloadInfo.fileName,
                            currentSize = _fileDownloadInfo.currentSize,
                            totalSize = _fileDownloadInfo.totalSize,
                            downloadFile = _fileDownloadInfo.downloadFile,
                            downloadingFile = _fileDownloadInfo.downloadingFile,
                            totalFile = _fileDownloadInfo.totalFile
                        });
                    });
                    break;
                }
            }
        }

        private string GetFileMD5(string filePath, Action<int> onProgress)
        {
            using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
            var buffer = new byte[4096];
            var numRead = 0;
            int totalRead = 0;
            using var md5 = MD5.Create();
            var prevTime = TimeUtil.GetNowTimestamp();
            do
            {
                numRead = fileStream.Read(buffer, 0, buffer.Length);
                totalRead += numRead;
                md5.TransformBlock(buffer, 0, numRead, null, 0);
                var nowTime = TimeUtil.GetNowTimestamp();
                if (nowTime - prevTime > 0.034) // 仅超过 1 帧的时候才向外抛出事件，避免大量抛出出现卡顿
                {
                    prevTime = nowTime;
                    onProgress?.Invoke(totalRead);
                }
            } while (numRead > 0);
            var hashString = String.Empty;
            for (int i = 0; i < md5.Hash.Length; i++)
            {
                hashString += Convert.ToString(md5.Hash[i], 16).PadLeft(2, '0');
            }
            return hashString.PadLeft(32, '0');
        }

        private void OnVerifyComplete()
        {
            SaveHotFixedVersion();
            onComplete?.Invoke();
        }
        
        /// <summary>
        /// 清空所有热更资源
        /// 一般常见登陆界面的修复按钮，修复热更出现的异常问题，重新进入游戏后重新走热更流程
        /// </summary>
        public void ClearHotFixed()
        {
            Directory.Delete(AssetUtil.HotFixedDir);
        }
    }

    /// <summary>
    /// 热更更新信息
    /// </summary>
    public class HotFixedChangeInfo
    {
        /// <summary>
        /// 当前步骤
        /// </summary>
        public HotFixedStep step;
        /// <summary>
        /// 错误码
        /// </summary>
        public HotFixedErrorCode error;
        /// <summary>
        /// 当前处理的文件名列表
        /// </summary>
        public List<string> fileName;
        /// <summary>
        /// 当前文件下载/校验的字节，多个文件合并后的数据
        /// </summary>
        public int currentSize;
        /// <summary>
        /// 当前文件下载/校验的总字节，多个文件合并后的数据
        /// </summary>
        public int totalSize;
        /// <summary>
        /// 已下载/校验的文件数量
        /// </summary>
        public int downloadFile;
        /// <summary>
        /// 当前正在下载/校验的文件数量
        /// </summary>
        public int downloadingFile;
        /// <summary>
        /// 需要下载/校验的文件总数量
        /// </summary>
        public int totalFile;
    }

    class HotFixedData
    {
        public string mainVersion;
        public int subVersion;
        public int hotFixedVersion;
        public string hotFixedManifestUrl;
        public string hotFixedUrl;
    }

    enum HotFixedChangeType
    {
        Add,
        Modify,
        Delete,
    }
    
    class HotFixedFile
    {
        public HotFixedChangeType changeType;
        public string fileName;
        public string url;
        public string diskPath;
        public string hashCode;
        public int size;
    }

    /// <summary>
    /// 下载/校验信息汇总
    /// </summary>
    class FileDownloadInfo
    {
        private Dictionary<string, FileDownload> downloadsMap = new Dictionary<string, FileDownload>();

        public List<string> fileName { get; } = new List<string>();

        public int currentSize
        {
            get
            {
                var downloadSize = 0;
                foreach (var kv in downloadsMap)
                {
                    var fileDownload = kv.Value;
                    if (fileDownload.state == FileDownloadState.Downloading)
                    {
                        downloadSize += fileDownload.downloadSize;
                    }
                }
                return downloadSize;
            }
        }
        
        public int totalSize
        {
            get
            {
                var totalSize = 0;
                foreach (var kv in downloadsMap)
                {
                    var fileDownload = kv.Value;
                    if (fileDownload.state == FileDownloadState.Downloading)
                    {
                        totalSize += fileDownload.totalSize;
                    }
                }
                return totalSize;
            }
        }

        public int downloadFile
        {
            get
            {
                var count = 0;
                foreach (var kv in downloadsMap)
                {
                    var fileDownload = kv.Value;
                    if (fileDownload.state == FileDownloadState.Complete)
                    {
                        count++;
                    }
                }
                return count;
            }
        }

        public int downloadingFile
        {
            get
            {
                var count = 0;
                foreach (var kv in downloadsMap)
                {
                    var fileDownload = kv.Value;
                    if (fileDownload.state == FileDownloadState.Downloading)
                    {
                        count++;
                    }
                }
                return count;
            }
        }

        public int totalFile => downloadsMap.Count;

        public bool allComplete
        {
            get
            {
                foreach (var kv in downloadsMap)
                {
                    var fileDownload = kv.Value;
                    if (fileDownload.state != FileDownloadState.Complete)
                    {
                        return false;
                    }
                }
                return true;
            }
        }
        
        public void AddDownload(string url, string fileName, int totalSize)
        {
            if (!downloadsMap.ContainsKey(url))
            {
                downloadsMap.Add(url, new FileDownload
                {
                    url = url,
                    fileName = fileName,
                    totalSize = totalSize
                });
            }
        }
        
        public void OnDownloadComplete(string url)
        {
            if (downloadsMap.TryGetValue(url, out var fileDownload))
            {
                if (fileName.Contains(fileDownload.fileName))
                {
                    fileName.Remove(fileDownload.fileName);
                }
                fileDownload.downloadSize = fileDownload.totalSize;
                fileDownload.state = FileDownloadState.Complete;
            }
        }

        public void OnDownLoadProgress(string url, long currentBytes)
        {
            if (downloadsMap.TryGetValue(url, out var fileDownload))
            {
                if (!fileName.Contains(fileDownload.fileName))
                {
                    fileName.Add(fileDownload.fileName);
                }
                fileDownload.state = FileDownloadState.Downloading;
                fileDownload.downloadSize = (int) currentBytes;
            }
        }

        public void OnDownLoadError(string url, string errorMsg)
        {
            if (downloadsMap.TryGetValue(url, out var fileDownload))
            {
                if (fileName.Contains(fileDownload.fileName))
                {
                    fileName.Remove(fileDownload.fileName);
                }
                fileDownload.state = FileDownloadState.Error;
                fileDownload.errorMsg = errorMsg;
            }
        }
    }

    public enum FileDownloadState
    {
        None,
        Downloading,
        Complete,
        Error,
    }
    
    class FileDownload
    {
        public string url;
        public string fileName;
        public int downloadSize;
        public int totalSize;
        public FileDownloadState state;
        public string errorMsg;
    }
}
